import random
import time

from utils.namespace import Namespace
from utils.clock import Clock
from utils.misc import INF, MAXINT

from khronos.statistics import TSeries, Plotter
 
from stacking.instance.generation.sim import Producer, Consumer, Simulator
from stacking.units import HOUR, DAY


class SimGen(object):
    """Base class for simulation-based instance generators."""
    def __init__(self, nproducts=1, learning_rate=0.25):
        self.rng = random.Random()
        self.production_interval = 0.5 * HOUR
        self.learning_rate = learning_rate
        self.products = ["Prod#%d" % x for x in xrange(nproducts)]
        self.producer = Producer(self.products, 
                                 self.production_interval_distr, 
                                 self.batch_size_distr)
        self.consumer = Consumer.create_xgg(self.products, 12 * HOUR, 2 * DAY, 6 * HOUR, 24, 4)
        self.simulator = Simulator([self.producer], [self.consumer])
        
        # +++ Collect values through iterations to show evolution of observed vs 
        # target difficulty criterion, and the evolution of production interval
        self.obs_tseries = TSeries()
        self.tgt_tseries = TSeries()
        self.pi_tseries  = TSeries()
        for tseries in self.obs_tseries, self.tgt_tseries, self.pi_tseries:
            tseries.time_fnc = lambda: len(tseries)
        self.pi_tseries.collect(self.production_interval)
        
    def production_interval_distr(self, rng):
        return rng.gauss(self.production_interval, self.production_interval * 0.1)
        
    def batch_size_distr(self, rng):
        return 1
        
    def generate(self, nitems, nstacks, seed=None, max_cpu=60.0, max_iterations=50, **kwargs):
        if seed is None:
            seed = time.time()
        self.rng.seed(seed)
        self.best = Namespace()
        self.best.delta = INF                 # minimum deviation from target indicator
        self.best.seed = seed                 # RNG seed used by the generator
        self.best.simseed = None              # RNG seed passed to the simulator
        self.best.production_interval = None  # production interval used in the simulation
        self.best.items = None                # set of items generated by the simulation
        self.best.iteration = None            # iteration when minimum deviation was achieved
        self.best.accepted = False            # True if the items met the specified criteria
        accepted = False   # when this becomes True, stop generating items
        iterations = 0
        clock = Clock()
        clock.start()
        self.configure(**kwargs)
        while not accepted and clock.total < max_cpu and iterations < max_iterations:
            current_production_interval = self.production_interval
            simseed = self.rng.randint(0, MAXINT)
            seed, items = self.simulator.generate(nitems, seed=simseed)
            accepted, delta = self.analyze(items, nstacks)
            if accepted or delta < self.best.delta:
                self.best.delta = delta
                self.best.simseed = simseed
                self.best.simgen_cpu = clock.total
                self.best.production_interval = current_production_interval
                self.best.items = items
                self.best.iteration = iterations + 1
                self.best.accepted = accepted
            iterations += 1
        clock.stop()
        return self.best
        
    def adjust_production_interval(self, target_production_interval):
        """Adjust the production interval in order to control the difficulty of the next set of 
        items generated by the simulation. This uses a learning rate parameter to control the 
        weight of the previous and target values of production interval in the computation of its 
        next value."""
        self.production_interval = ((1.0 - self.learning_rate) * self.production_interval + 
                                    self.learning_rate * target_production_interval)
        self.pi_tseries.collect(self.production_interval)
        
    def configure(self, **kwargs):
        """Configure the generator with arguments passed to generate() before the start of the 
        main loop.""" 
        raise NotImplementedError()
        
    def analyze(self, items, nstacks):
        """This method should analyze a set of items generated by simulation, and return a pair 
        with an 'accepted' flag, and a (real) number representing the deviation from the target 
        difficulty."""
        raise NotImplementedError()
        
    def debug_plot(self):
        plt = Plotter()
        self.pi_tseries.line_plot(target=plt.add_axes())
        axes = plt.add_axes()
        for tseries in [self.obs_tseries, self.tgt_tseries]:
            tseries.line_plot(target=axes)
        for axes in plt.figure.axes:
            axes.grid()
        plt.layout(rows=2, cols=1)
        return plt
        
        